TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1'use client';23import { useActionState } from 'react';4import { addItemAction, updateItemAction } from '@/lib/account/actions';5import { idle, type ActionState } from '@/lib/auth/state';6import { Field, FormMessage, Select, SubmitButton, TextArea, TextInput } from '@/components/account/form';7import { AssetPicker, type PickedAsset } from '@/components/account/asset-picker';89const CURRENCIES = ['USD', 'CAD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD'];10const GRADERS = ['', 'psa', 'bgs', 'cgc', 'sgc', 'tag', 'ace', 'cbcs', 'wata', 'vga', 'pcgs', 'ngc'];1112export interface ItemInitial {13 itemId: string;14 asset: PickedAsset;15 quantity: number;16 acquiredAt: string | null;17 purchasePrice: number | null;18 purchaseCurrency: string | null;19 source: string | null;20 grader: string | null;21 grade: string | null;22 certificationNumber: string | null;23 serial: string | null;24 condition: string | null;25 notes: string | null;26 tags: string[];27 manualValueUsd: number | null;28 soldAt: string | null;29 soldPriceUsd: number | null;30}3132export function AddItemForm({ collectionId }: { collectionId: string }) {33 const [state, action] = useActionState(addItemAction, idle);34 return <ItemFields state={state} action={action} collectionId={collectionId} submitLabel="Add to collection" />;35}3637export function EditItemForm({ initial }: { initial: ItemInitial }) {38 const [state, action] = useActionState(updateItemAction, idle);39 return <ItemFields state={state} action={action} initial={initial} submitLabel="Save item" />;40}4142function ItemFields({ state, action, collectionId, initial, submitLabel }: { state: ActionState; action: (fd: FormData) => void; collectionId?: string; initial?: ItemInitial; submitLabel: string }) {43 return (44 <form action={action} className="space-y-4" noValidate key={state.ok && !initial ? String(state.data?.itemId) : 'form'}>45 {collectionId ? <input type="hidden" name="collectionId" value={collectionId} /> : null}46 {initial ? <input type="hidden" name="itemId" value={initial.itemId} /> : null}47 <FormMessage state={state} />48 {initial ? <input type="hidden" name="assetId" value={initial.asset.id} /> : null}49 <AssetPicker initial={initial?.asset ?? null} error={state.fieldErrors?.assetId} withVariant={!initial} />50 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">51 <Field label="Quantity" name="quantity" error={state.fieldErrors?.quantity}>52 <TextInput name="quantity" type="number" min={1} max={10000} defaultValue={initial?.quantity ?? 1} required />53 </Field>54 <Field label="Acquired on" name="acquiredAt" error={state.fieldErrors?.acquiredAt}>55 <TextInput name="acquiredAt" type="date" defaultValue={initial?.acquiredAt ?? ''} max={new Date().toISOString().slice(0, 10)} />56 </Field>57 <Field label="Purchase price (per unit)" name="purchasePrice" error={state.fieldErrors?.purchasePrice}>58 <TextInput name="purchasePrice" type="number" min={0} step="0.01" defaultValue={initial?.purchasePrice ?? ''} placeholder="0.00" />59 </Field>60 <Field label="Currency" name="purchaseCurrency" hint="Converted to USD at the acquisition-date rate.">61 <Select name="purchaseCurrency" defaultValue={initial?.purchaseCurrency ?? 'USD'}>62 {CURRENCIES.map((c) => (63 <option key={c} value={c}>64 {c}65 </option>66 ))}67 </Select>68 </Field>69 </div>70 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">71 <Field label="Grader" name="grader" hint="Used when no variant is selected.">72 <Select name="grader" defaultValue={initial?.grader ?? ''}>73 {GRADERS.map((g) => (74 <option key={g} value={g}>75 {g ? g.toUpperCase() : 'Raw / none'}76 </option>77 ))}78 </Select>79 </Field>80 <Field label="Grade" name="grade">81 <TextInput name="grade" defaultValue={initial?.grade ?? ''} placeholder="10, 9.5, 9.8, MS65…" maxLength={12} />82 </Field>83 <Field label="Certification #" name="certificationNumber">84 <TextInput name="certificationNumber" defaultValue={initial?.certificationNumber ?? ''} maxLength={40} className="mono-num" />85 </Field>86 <Field label="Serial / reference" name="serial">87 <TextInput name="serial" defaultValue={initial?.serial ?? ''} maxLength={60} className="mono-num" />88 </Field>89 </div>90 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">91 <Field label="Condition" name="condition">92 <TextInput name="condition" defaultValue={initial?.condition ?? ''} placeholder="sealed, CIB, unworn, full set…" maxLength={40} />93 </Field>94 <Field label="Source" name="source">95 <TextInput name="source" defaultValue={initial?.source ?? ''} placeholder="eBay, Heritage, local show…" maxLength={120} />96 </Field>97 <Field label="Tags" name="tags" hint="Comma-separated, up to 12.">98 <TextInput name="tags" defaultValue={initial?.tags.join(', ') ?? ''} placeholder="grail, sealed, gift" maxLength={200} />99 </Field>100 <Field label="Manual value (USD)" name="manualValueUsd" hint="Fallback only when RareIndex has no valuation; never shown as RIV.">101 <TextInput name="manualValueUsd" type="number" min={0} step="0.01" defaultValue={initial?.manualValueUsd ?? ''} />102 </Field>103 </div>104 {initial ? (105 <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">106 <Field label="Sold on (optional)" name="soldAt">107 <TextInput name="soldAt" type="date" defaultValue={initial.soldAt ?? ''} />108 </Field>109 <Field label="Sold price (USD)" name="soldPriceUsd">110 <TextInput name="soldPriceUsd" type="number" min={0} step="0.01" defaultValue={initial.soldPriceUsd ?? ''} />111 </Field>112 </div>113 ) : null}114 <Field label="Notes" name="notes">115 <TextArea name="notes" defaultValue={initial?.notes ?? ''} maxLength={2000} placeholder="Provenance, storage location, insurance reference…" className="min-h-[70px]" />116 </Field>117 <SubmitButton pendingText="Saving…">{submitLabel}</SubmitButton>118 </form>119 );120}121